/**
 * Copyright 2011 AJG van Schie
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package com.googlecode.julius.data;

import static julius.utilities.GeneralObjectHelper.getOrDefault;

import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import julius.utilities.CollectionHelper;

import org.apache.log4j.Logger;
import org.joda.time.DateTime;
import org.joda.time.LocalDate;

import com.googlecode.julius.controller.ControllerApi.Sprint;
import com.googlecode.julius.data.define.DefineIssue;
import com.googlecode.julius.data.define.DefineMilestone;
import com.googlecode.julius.data.define.DefineTicketRef;
import com.googlecode.julius.data.define.SprintPeriod;
import com.googlecode.julius.utils.DateHelper;
import com.googlecode.julius.utils.ParallelForEach;
import com.googlecode.julius.utils.ParallelForEach.ParallelCall;
import com.googlecode.julius.utils.ParallelForEach.ProgressCallback;

public class ReaderApiImpl implements DataReaderApi {
    public static Logger log = Logger.getLogger(ReaderApiImpl.class);

    public static class SprintImpl implements Sprint {

        private final String name;
        private final LocalDate start;
        private final LocalDate end;
        private final SprintMeta meta;

        public SprintImpl(final String name, final SprintMeta meta) {
            super();
            this.name = name;
            this.meta = meta;
            this.start = meta.getStart();
            this.end = meta.getEnd();
        }

        @Override
        public String name() {
            return name;
        }

        @Override
        public LocalDate start() {
            return start;
        }

        @Override
        public LocalDate end() {
            return end;
        }

        public SprintMeta getMeta() {
            return meta;
        }

        @Override
        public List<LocalDate> daysInSprint() {
            return CollectionHelper.allExceptLast(DateHelper.getSprintDays(start, end)); // last day is not part of sprint...
        }

        @Override
        public String toString() {
            return name + " | " + DateHelper.friendly(start) + " - " + DateHelper.friendly(end) + "";
        }

    }

    class Contents implements SprintContents {

        private final Sprint sprint;
        private final Map<Status, Map<LocalDate, Double>> hours;

        public Contents(final Sprint sprint, final Map<Status, Map<LocalDate, Double>> hours) {
            super();
            this.sprint = sprint;
            this.hours = hours;
        }

        @Override
        public Sprint sprint() {
            return sprint;
        }

        @Override
        public Double hours(final LocalDate date, final Status state) {
            Map<LocalDate, Double> datemap = (Map<LocalDate, Double>) getOrDefault(hours.get(state), CollectionHelper.map());
            return datemap.get(date);
        }

    }

    private final DefineReader reader;
    private final Map<SprintImpl, DefineMilestone> uri = CollectionHelper.map();

    public ReaderApiImpl(final DefineReader reader) {
        super();
        this.reader = reader;
    }

    @Override
    public List<Sprint> readSprints() {
        List<Sprint> sprints = CollectionHelper.list();
        for (DefineMilestone stone : reader.readMilestones()) {
            try {
                SprintImpl sprint = new SprintImpl(stone.getName(), reader.readMeta(stone));
                sprints.add(sprint);
                uri.put(sprint, stone);
            } catch (RuntimeException e) {
                log.info("skip sprint " + stone + " " + e);
                // ignore this, because a lot of sprints wont have the meta data
            }
        }
        return sprints;
    }

    private Map<LocalDate, Double> calculateExpected(final SprintImpl sprint) {
        List<LocalDate> daysFromEndToStart = new LinkedList<LocalDate>(sprint.daysInSprint());
        Collections.reverse(daysFromEndToStart); // TODO AS: Julius (zou een copy moeten reversen (ivm immutable))
        Map<LocalDate, Double> expect = CollectionHelper.map();
        Double currentSum = 0.0d;
        for (LocalDate day : daysFromEndToStart) {
            expect.put(day, currentSum);
            currentSum = currentSum + Double.valueOf(sprint.getMeta().getHoursOnDay(day));
        }
        return expect;
    }

    @Override
    public SprintContents readSprintContents(final Sprint sprint, final ProgressCallback c) {
        Collection<DefineIssue> items = readIssuesAsync(sprint, c);
        SprintPeriod period = new SprintPeriod(sprint.start(), sprint.end());
        Map<Status, Map<LocalDate, Double>> raw = getHistogramCummulative(period, items);
        Map<LocalDate, Double> exp = calculateExpected((SprintImpl) sprint); // TODO
        raw.put(Status.Expected, exp);
        return new Contents(sprint, raw);
    }

    private Collection<DefineIssue> readIssuesAsync(final Sprint sprint, final ProgressCallback callback) {
        ParallelCall<DefineIssue, DefineTicketRef> call = new ParallelCall<DefineIssue, DefineTicketRef>() {
            @Override
            public DefineIssue map(final DefineTicketRef ticket) {
                return reader.readItem(ticket.getId());
            }
        };
        ParallelForEach<DefineIssue, DefineTicketRef> parForEach = new ParallelForEach<DefineIssue, DefineTicketRef>(2, callback,
                false, 5);
        Collection<DefineTicketRef> tickets = reader.readMilestone(uri.get(sprint)).getIssues();
        return parForEach.map(tickets, call);
    }

    // TODO Below use multimap or create MapMap

    protected Map<Status, Map<LocalDate, Double>> getHistogram(final SprintPeriod period, final Collection<DefineIssue> items) {
        Map<Status, Map<LocalDate, Double>> statusHoursHistory = CollectionHelper.map();
        for (DefineIssue issue : items) {
            Map<LocalDate, Double> issueHours = issue.getHourOverviewUpTo(period.getStart(), new DateTime().toLocalDate());
            Map<LocalDate, Status> issueStates = issue.getStatusOverviewUpTo(new DateTime().toLocalDate());
            double end = 0d;
            for (LocalDate cursor : period.getSprintDays()) {
                Status stateCursor = issueStates.get(cursor);
                Map<LocalDate, Double> stateHours = getOrDefault(statusHoursHistory.get(stateCursor),
                        new HashMap<LocalDate, Double>());
                Double currentTotal = stateHours.get(cursor);
                Double itemSize = issueHours.get(cursor);
                stateHours.put(cursor, getOrDefault(currentTotal, 0d) + getOrDefault(itemSize, 0d));
                statusHoursHistory.put(stateCursor, stateHours);
                if (itemSize != null) {
                    end = itemSize;
                }

            }
            log.info(issue + " " + end);
        }
        return statusHoursHistory;
    }

    protected Map<Status, Map<LocalDate, Double>> getHistogramCummulative(final SprintPeriod period,
            final Collection<DefineIssue> items) {
        Map<Status, Map<LocalDate, Double>> statusHoursHistory = getHistogram(period, items);
        Map<Status, Map<LocalDate, Double>> copy = CollectionHelper.map();
        for (Map.Entry<Status, Map<LocalDate, Double>> stateMap : statusHoursHistory.entrySet()) {
            if (stateMap.getKey() != null) {
                copy.put(stateMap.getKey(), new HashMap<LocalDate, Double>(stateMap.getValue()));
                for (Status lowerStatus : stateMap.getKey().getAllBefore()) {
                    Map<LocalDate, Double> hoursCurrentState = copy.get(stateMap.getKey());
                    for (LocalDate cursor : period.getSprintDays()) {
                        Double currentTotal = getOrDefault(hoursCurrentState.get(cursor), 0d);
                        Map<LocalDate, Double> mapDay = getOrDefault(statusHoursHistory.get(lowerStatus),
                                new HashMap<LocalDate, Double>());
                        // Double newVal = getOrDefault(mapDay.get(cursor), 0d);
                        Double newVal = mapDay.get(cursor);
                        if (newVal != null) {
                            hoursCurrentState.put(cursor, currentTotal + newVal);
                        }
                    }
                }
            }
        }
        return copy;
    }

}
